<?php
// $Id$

/**
 * @file
 * Provides batch functions to create and update player nodes using armory data.
 */

// World of Warcraft URLs.
define('ARMORY_CHARACTER_URL', 'http://%s.wowarmory.com/character-sheet.xml?r=%s&n=%s');
define('ARMORY_GUILD_URL', 'http://%s.wowarmory.com/guild-info.xml?r=%s&n=%s');
define('ARMORY_ITEM_URL', 'http://%s.wowarmory.com/item-info.xml?i=%d');

// Get Firefox.
define('ARMORY_USERAGENT', 'Firefox/3.6');

// Generic armory exception. More specific armory exceptions extend this one.
// See http://drupal.org/node/608166 for conventions.
class ArmoryException extends Exception {}

class ArmoryIOException extends ArmoryException {}
class ArmoryHttpException extends ArmoryException {}
class ArmoryNotFoundException extends ArmoryException {}
class ArmoryXmlException extends ArmoryException {}

/**
 * Get a file from the specified server.
 *
 * @param $url A fully qualified URI.
 * @return The data returned from the server at the given url.
 */
function armory_load_file($url) {
  // You get normal html without a valid user agent.
  $headers = array('User-Agent' => ARMORY_USERAGENT);
  $response = drupal_http_request($url, $headers);
  
  if (isset($response->error)) {
    throw new ArmoryIOException("Unable to contact the server at $url.");
  }
  
  return $response->data;
}

/**
 * Process character XML data into a more usable format.
 *
 * @param $url The location of the character XML resource.
 *
 * @return mixed
 *   An associative array containing a list of all character details, or FALSE if the 
 *   character could not be found.
 */
function armory_parse_character_xml($url) {
  $data = armory_load_file($url);
  
  // @todo See drupal_xml_parser_create().
  
  // Load character-sheet xml.
  $xml = NULL;
  try {
    $xml = new SimpleXMLElement($data);
  }
  catch (Exception $e) {
    throw new ArmoryXmlException($e->getMessage());
  }
  
  if (!isset($xml)) {
    throw new ArmoryException();
  }
  else if (isset($xml->errorhtml)) {
    // We can connect, but the server won't process our request (HTTP 503). <errorhtml />
    throw new ArmoryHttpException('HTTP 503: Service Unavailable');
  }
  else if (isset($xml->characterInfo['errCode'])) {
    // Our search did not produce any results. <characterInfo errCode="noCharacter" />
    throw new ArmoryNotFoundException();
  }
  
  // Create our data array.
  $character = array();
  foreach ($xml->characterInfo->character[0]->attributes() as $key => $info) {
    // Force the data to a string so we don't get SimpleXMLElement objects.
    $character[$key] = (string)$info;
  }
  
  return $character;
}

/**
 * Process guild XML into a more usable format containing guild information
 * and a list of guild members.
 *
 * @param $url The location of the guild XML resource.
 *
 * @return mixed
 *   An associative array containing guild information and members, or FALSE if the 
 *   guild could not be found.
 */
function armory_parse_guild_xml($url) {
  $data = armory_load_file($url);
  
  $xml = NULL;
  try {
    $xml = new SimpleXMLElement($data);
  }
  catch (Exception $ex) {
    throw new ArmoryXmlException($ex->getMessage());
  }
  
  if (!isset($xml)) {
    throw new ArmoryException();
  }
  else if (isset($xml->errorhtml)) {
    // We can connect, but the server won't process our request (HTTP 503). <errorhtml />
    throw new ArmoryHttpException('HTTP 503: Service Unavailable');
  }
  else if (!isset($xml->guildInfo->guild)) {
    // Our search did not produce any results. <guildInfo />
    throw new ArmoryNotFoundException();
  }
  
  $guild = array(
    'info' => array(),
    'characters' => array()
  );
  
  // Create our guild info array.
  foreach ($xml->guildKey[0]->attributes() as $key => $info) {
    $guild['info'][$key] = (string)$info;
  }
  
  // Tack on the actual member count (includes members below level 10).
  $member_count = $xml->guildInfo->guild->members['memberCount'];
  $guild['info']['memberCount'] = (string)$member_count;
  
  // Create our guild member arrays.
  foreach ($xml->guildInfo->guild->members->character as $member) {
    $character = array();
    
    foreach ($member->attributes() as $key => $info) {
      $character[$key] = (string)$info;
    }
    
    $guild['characters'][] = $character;
  }
  
  return $guild;
}

/**
 * Update all player nodes using guild data from the World of Warcraft Armory.
 *
 * @param $realm The name of the realm the guild plays on.
 * @param $guild The name of the guild.
 * @param $region (optional) The realm's region identifier.
 */
function armory_batch_import_guild($realm, $guild, $region = 'www') {
  $guild_url = sprintf(ARMORY_GUILD_URL, $region, urlencode($realm), urlencode($guild));
  
  // Create and start our batch operation.
  // Possible placeholders: @current, @remaining, @total, @percent
  $batch = array(
    'operations' => array(
      array('_armory_batch_import_guild_xml', array($guild_url)),
      array('_armory_batch_import_guild_process', array()),
      array('_armory_batch_import_guild_ranks', array())
    ),
    'file' => drupal_get_path('module', 'wow') . '/includes/wow.armory.inc',
    'title' => t('Armory import'),
    'finished' => '_armory_batch_import_guild_finished',
    'init_message' => t('Gathering armory data...'),
    'progress_message' => t('Processing armory data...'),
    'error_message' => t('Armory import has encountered an error.')
  );
  
  batch_set($batch);
}

/**
 * Batch operation; Retrieves guild information from the armory.
 *
 * @param $url The URL to guild xml data.
 */
function _armory_batch_import_guild_xml($url, &$context) {
  $context['results']['error'] = '';
  
  try {
    // Aren't exceptions fun!
    $guild = armory_parse_guild_xml($url);
    $context['results']['guild_info'] = $guild;
  }
  catch (ArmoryHttpException $ex) {
    $context['results']['error'] = t('The armory server is busy. Please try again later.');
  }
  catch (ArmoryIOException $ex) {
    $context['results']['error'] = t('Unable to connect. The armory server may be unavailable.');
  }
  catch (ArmoryXmlExpcetion $ex) {
    $context['results']['error'] = t('An error occured while processing armory xml data.');
  }
  catch (ArmoryNotFoundException $ex) {
    $context['results']['error'] = t('Unable to get guild information. Please check that the realm name and guild name are correct.');
  }
  catch (ArmoryException $ex) {
    $context['results']['error'] = t('An unknown error has occurred while retrieving armory information.');
  }
}

/**
 * Batch operation; Create or update player node data.
 */
function _armory_batch_import_guild_process(&$context) {
  // If the guild is big enough, it may actually crash the database?
  // "PHP Warning:  MySQL server has gone away" + watchdog = loop :(
  if (isset($context['results']['guild_info'])) {
    $context['sandbox']['guild_info'] = $context['results']['guild_info'];
    unset($context['results']['guild_info']);
  }
  
  // Although unlikely, this might be NULL if there was a problem getting the xml,
  // in which case max would be set to 0, effectively ending the function anyways.
  $guild = $context['sandbox']['guild_info'];
  
  if (!isset($context['sandbox']['progress'])) {
    $context['message'] = t("Updating player attributes.");
    
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = count($guild['characters']);
    
    $context['results']['existing_players'] = 0;
    $context['results']['new_players'] = 0;
    
    $context['results']['member_ranks'] = array();
    
    // Reset all players to hidden; if they are still in the guild they will
    // be set to active again later. Inactive members should be restored,
    // if automatic inactivity is enabled.
    if ($context['sandbox']['max'] != 0) {
      db_query("UPDATE {guild_member} SET status = '%d'", GUILD_PLAYER_HIDDEN);
    }
  }
  
  // Slowly process guild members.
  $limit = 20;
  while ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    if ($limit == 0) {
      break;
    }
    
    $i = $context['sandbox']['progress'];
    $player = $guild['characters'][$i];
    
    $result = db_fetch_object(db_query(
      "SELECT n.nid FROM {node} n " .
      "INNER JOIN {guild_member} m USING(nid) WHERE LOWER(n.title) = '%s'",
      drupal_strtolower($player['name']))
    );
    
    $info = array();
    $info['player_class'] = $player['classId'];
    $info['player_gender'] = $player['genderId'];
    $info['player_level'] = $player['level'];
    $info['player_race'] = $player['raceId'];
    $info['member_status'] = GUILD_PLAYER_ACTIVE;
    
    if ($result) {
      guild_update_player($result->nid, $info);
      $context['results']['existing_players']++;
      
      $context['results']['member_ranks'][$i] = array(
        'nid' => $result->nid,
        'tier' => $player['rank']
      );
    }
    else {
      $node = guild_create_player($player['name'], $info);
      $context['results']['new_players']++;
      
      $context['results']['member_ranks'][$i] = array(
        'nid' => $node->nid,
        'tier' => $player['rank']
      );
    }
    
    $context['sandbox']['progress']++;
    $count--;
  }
  
  // Update progress.
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * @todo Make sure the number of ranks does not exceed 10.
 * @todo Consider including a "former member" rank.
 */
function _armory_batch_import_guild_ranks(&$context) {
  // @todo Test me.
  if (isset($context['results']['member_ranks'])) {
    $context['sandbox']['member_ranks'] = $context['results']['member_ranks'];
    unset($context['results']['member_ranks']);
  }
  
  $member_ranks = $context['sandbox']['member_ranks'];
  
  if (!isset($context['sandbox']['progress'])) {
    $context['message'] = t('Setting guild member ranks.');
    
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = count($member_ranks);
    
    $max_armory_tier = -1;
    
    // Find out how many ranks the guild is using.
    foreach ($member_ranks as $key) {
      if ($key['tier'] > $max_armory_tier) {
        $max_armory_tier++;
      }
    }
    
    // That's odd, there should have been a guild master.
    // Prevent old ranks from being removed if there was a problem earlier.
    if ($max_armory_tier == -1) {
      $context['finished'] = 1;
      return;
    }
    
    // Clean up our ranks.
    
    $query = "SELECT MAX(r.tier) AS max FROM {guild_rank} r WHERE r.module = '%s'";
    $max_tier = db_result(db_query($query, 'wow'));
    
    // There were no previously defined ranks for this module.
    if (!isset($max_tier) || !is_numeric($max_tier)) {
      $max_tier = 0;
    }
    
    if ($max_armory_tier < $max_tier) {
      // Since players that are no longer part of the guild cannot be updated
      // and are not removed, we need to keep their rank data valid here.
      db_query("UPDATE {guild_member} m SET m.rank_id = 0 WHERE m.rank_id " .
        "IN(SELECT r.rank_id FROM {guild_rank} r WHERE r.module = '%s' AND r.tier >= %d)",
        'wow', $max_armory_tier);
      
      // Remove unused ranks.
      db_query("DELETE FROM {guild_rank} WHERE module = '%s' AND tier > %d",
        'wow', $max_armory_tier);
    }
    else if ($max_armory_tier != $max_tier) {
      // We need more ranks.
      for ($i = $max_tier; $i <= $max_armory_tier; $i++) {
        $rank_name = ($i == 0) ? t('Guild Leader') : t('Rank ') . $i;
        
        db_query("INSERT INTO {guild_rank} (name, tier, locked, module) VALUES ('%s', %d, %d, '%s')",
          $rank_name, $i, GUILD_RANK_TIER_LOCK, 'wow');
      }
    }
    
    // Save all our ranks for other iterations of this function.
    $result = db_query("SELECT * FROM {guild_rank} r WHERE r.module = '%s'", 'wow');
    while ($rank = db_fetch_object($result)) {
      $context['sandbox']['ranks'][$rank->tier] = $rank->rank_id;
    }
  }
  
  $limit = 20;
  while ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    if ($limit == 0) {
      break;
    }
    
    $i = $context['sandbox']['progress'];
    $ranks = $context['sandbox']['ranks'];
    
    db_query("UPDATE {guild_member} m SET m.rank_id = %d WHERE m.nid = %d",
      $ranks[$member_ranks[$i]['tier']], $member_ranks[$i]['nid']);
    
    $context['sandbox']['progress']++;
    $count--;
  }
  
  // Update progress.
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Batch finish; Display results of the armory guild import.
 */
function _armory_batch_import_guild_finished($success, $results, $operations) {
  //$message = theme('item_list', $results);
  
  if ($success) {
    if (empty($results['error'])) {
      // Don't count an error, if we used count($results).
      //unset($results['error']);
      
      $existing_players = $results['existing_players'];
      $new_players = $results['new_players'];
      
      if ($existing_players != 0) {
        drupal_set_message(format_plural($existing_players,
          'Updated @count existing player.', 'Updated @count existing players.',
          array('@count' => $existing_players)), 'status');
      }
      
      if ($new_players != 0) {
        drupal_set_message(format_plural($new_players,
          'Created @count new player.', 'Created @count new players.',
          array('@count' => $new_players)), 'status');
      }
      
      return;
    }
    else {
      $message = $results['error'];
    }
  }
  else {
    $message = t('An unknown error has occurred while processing guild information.');
  }
  
  drupal_set_message($message, 'error');
}
